/**
 * Copyright (C) 2010 - 2013 Harry Glasgow
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.jaden.engine;

import com.googlecode.jaden.common.config.IOConfiguration;
import com.googlecode.jaden.common.enums.DateResultType;
import com.googlecode.jaden.common.enums.IOType;
import com.googlecode.jaden.common.other.JadenException;
import com.googlecode.jaden.common.other.Utils;

import static com.googlecode.jaden.ui.Constants.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Date;
import java.util.Calendar;
import java.text.SimpleDateFormat;
import java.text.ParseException;

public class IOUnit {

    private final IOConfiguration config;
    private final double numericInternalA;
    private final double numericInternalB;
    private final double numericExternalA;
    private final double numericExternalB;
    private final Map<String, Double> cyclicSine = new HashMap<String, Double>();
    private final Map<String, Double> cyclicCosine = new HashMap<String, Double>();

    public IOUnit(IOConfiguration config) throws IllegalStateException {
        this.config = config;
        if (config.getType() == IOType.Numeric) {
            int max = config.getNumericMaxValue();
            int min = config.getNumericMinValue();
            numericInternalA = 2.0 / (max - min);
            numericInternalB = -((double) max + (double) min) / ((double) max - (double) min);
            numericExternalA = (max - min) / 2.0;
            numericExternalB = (max + min) / 2.0;
        } else if (config.getType() == IOType.Categorical) {
            if (config.isOutputConfig() && config.isCategoricalCyclic()) {
                throw new IllegalStateException("Cyclic output?");
            }
            numericInternalA = 0; // N/A
            numericInternalB = 0; // N/A
            numericExternalA = 0; // N/A
            numericExternalB = 0; // N/A
            if (config.isCategoricalCyclic()) {
                double scale = 2.0 * Math.PI / config.getCategoricalList().size();
                double v = 0.0;
                for (String s : config.getCategoricalList()) {
                    cyclicSine.put(s, StrictMath.sin(v));
                    cyclicCosine.put(s, StrictMath.cos(v));
                    v += scale;
                }
            }
        } else if (config.getType() == IOType.Map) {
            if (config.isOutputConfig()) {
                throw new IllegalStateException("Map output?");
            }
            double max = -Double.MAX_VALUE;
            double min = Double.MAX_VALUE;
            for (Double value : config.getMap().values()) {
                if (value > max) {
                    max = value;
                }
                if (value < min) {
                    min = value;
                }
            }
            numericInternalA = 2.0 / (max - min);
            numericInternalB = -(max + min) / (max - min);
            numericExternalA = 0; // N/A
            numericExternalB = 0; // N/A
        } else if (config.getType() == IOType.Date) {
            if (config.isOutputConfig()) {
                throw new IllegalStateException("Date output?");
            }
            numericInternalA = 0; // N/A
            numericInternalB = 0; // N/A
            numericExternalA = 0; // N/A
            numericExternalB = 0; // N/A

            if (config.getDateResultType() != DateResultType.DayOfWeekCategorical &&
                    config.getDateResultType() != DateResultType.MonthCategorical) {
                int range;
                switch (config.getDateResultType()) {
                    case MonthOfYear:
                        range = 12;
                        break;
                    case DayOfYear:
                        range = 366;
                        break;
                    case DayOfMonth:
                        range = 31;
                        break;
                    case DayOfWeek:
                        range = 7;
                        break;
                    case HourOfDay:
                        range = 24;
                        break;
                    case MinuteOfHour:
                        range = 60;
                        break;
                    case SecondOfMinute:
                        range = 60;
                        break;
                    default:
                        throw new IllegalStateException("Date resultType " + config.getDateResultType());
                }
                for (int i = 0; i < range; i++) {
                    double v = i * 2.0 * Math.PI / range;
                    cyclicSine.put(String.valueOf(i), StrictMath.sin(v));
                    cyclicCosine.put(String.valueOf(i), StrictMath.cos(v));
                }
            }
        } else {
            throw new IllegalStateException("Type? " + config.getType());
        }
    }

    public double[] toInternal(String value) throws JadenException {
        if (config.getType() == IOType.Numeric) {
            return new double[]{Double.parseDouble(value) * numericInternalA + numericInternalB};
        } else if (config.getType() == IOType.Categorical) {
            if (config.isCategoricalCyclic()) {
                Double sin = cyclicSine.get(value);
                if (sin == null) {
                    throw new IllegalStateException("Failed to find a cyclic (categorical) value for " + value);
                }
                double[] q = new double[2];
                q[0] = sin;
                q[1] = cyclicCosine.get(value);
                return q;
            } else {
                int i = 0;
                double[] q = new double[getInternalWidth()];
                boolean found = false;
                for (String s : config.getCategoricalList()) {
                    q[i] = s.equals(value) ? 1 : -1;
                    if (s.equals(value)) {
                        found = true;
                    }
                    i++;
                }
                if (!found) {
                    throw new IllegalStateException("Failed to find a value for " + value);
                }
                return q;
            }
        } else if (config.getType() == IOType.Map) {
            for (Map.Entry<String, Double> entry : config.getMap().entrySet()) {
                if (entry.getKey().equals(value)) {
                    return new double[]{entry.getValue() * numericInternalA + numericInternalB};
                }
            }
            throw new JadenException(Utils.translate(ERROR_NOT_MAP_KEY, value, config.getName()));
        } else if (config.getType() == IOType.Date) {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat(config.getDateFormat());
                Date date = sdf.parse(value);
                Calendar cal = Calendar.getInstance();
                cal.setTime(date);
                int part = extractPartNumber(config.getDateResultType(), cal);
                if (config.getDateResultType() == DateResultType.DayOfWeekCategorical) {
                    double[] q = new double[7];
                    for (int i = 0; i < q.length; i++) {
                        q[i] = -1;
                    }
                    q[part] = 1;
                    return q;
                } else if (config.getDateResultType() == DateResultType.MonthCategorical) {
                    double[] q = new double[12];
                    for (int i = 0; i < q.length; i++) {
                        q[i] = -1;
                    }
                    q[part] = 1;
                    return q;
                } else {
                    String partString = String.valueOf(part);
                    Double sin = cyclicSine.get(partString);
                    if (sin == null) {
                        throw new IllegalStateException("Failed to find a cyclic (date) value for " + value);
                    }
                    double[] q = new double[2];
                    q[0] = sin;
                    q[1] = cyclicCosine.get(partString);
                    return q;
                }
            } catch (ParseException e) {
                throw new IllegalStateException("Could not parse date " + value);
            }
        } else {
            throw new IllegalStateException("Type? " + config.getType());
        }
    }

    /**
     * Extracts appropriate 0 to n - 1 for a date type for a date.
     *
     * @param dateResultType
     * @param cal
     * @return
     */
    static int extractPartNumber(DateResultType dateResultType, Calendar cal) {
        int key;
        int offset = -1; // Offset to convert 1 to n, into 0 to n-1.
        switch (dateResultType) {
            case MonthOfYear:
                key = Calendar.MONTH;
                offset = 0; // Java Month is already 0 (January) to 11 (December).
                break;
            case DayOfYear:
                key = Calendar.DAY_OF_YEAR;
                break;
            case DayOfMonth:
                key = Calendar.DAY_OF_MONTH;
                break;
            case DayOfWeek:
                key = Calendar.DAY_OF_WEEK;
                break;
            case HourOfDay:
                key = Calendar.HOUR_OF_DAY;
                offset = 0; // Java hour is already 0 to 23.
                break;
            case MinuteOfHour:
                key = Calendar.MINUTE;
                offset = 0; // Java minute is already 0 to 59.
                break;
            case SecondOfMinute:
                key = Calendar.SECOND;
                offset = 0; // Java second is already 0 to 59.
                break;
            case DayOfWeekCategorical:
                key = Calendar.DAY_OF_WEEK;
                break;
            case MonthCategorical:
                key = Calendar.MONTH;
                offset = 0; // Java Month is already 0 (January) to 11 (December).
                break;
            default:
                throw new IllegalStateException("Date resultType " + dateResultType);
        }
        int part = cal.get(key);
        return part + offset;
    }

    public String toExternal(double[] value) throws IllegalStateException {
        if (config.getType() == IOType.Numeric) {
            return String.valueOf(value[0] * numericExternalA + numericExternalB);
        } else if (config.getType() == IOType.Categorical) {
            if (config.isOutputConfig()) {
                double max = -Double.MAX_VALUE;
                String r = "";
                int i = 0;
                for (double v : value) {
                    if (v > max) {
                        max = v;
                        r = config.getCategoricalList().get(i);
                    }
                    i++;
                }
                return r;
            } else {
                throw new IllegalStateException("CategoricalOutputType?");
            }
        } else if (config.getType() == IOType.Map) {
            throw new IllegalStateException("Should not be doing toExternal on a map");
        } else if (config.getType() == IOType.Date) {
            throw new IllegalStateException("Should not be doing toExternal on a date");
        } else {
            throw new IllegalStateException("Type? " + config.getType());
        }
    }

    public int getInternalWidth() {
        return config.getInternalWidth();
    }

    public boolean isCategoricalOutputType() {
        return config.getType() == IOType.Categorical && config.isOutputConfig();
    }

    public double[] toError(double[] internalTarget, double[] internalActual) {
        double[] results = new double[config.getInternalWidth()];
        if (config.getType() == IOType.Numeric) {
            return new double[]{internalTarget[0] - internalActual[0]};
        } else if (config.getType() == IOType.Categorical) {
            for (int i = 0; i < results.length; i++) {
                results[i] = internalTarget[i] - internalActual[i];
            }
            return results;
        }
        throw new IllegalStateException("toError for " + config.getType());
    }
}
